How to make unit tests with several localizations
Since Xcode 11 there is a possibility to configure different locales for unit tests by using Test Plans. It is fine, but a little bit cumbersome as requires the Xcode-specific configuration. Another way of making it work is the old-school method swizzling. It is quite simple to do and does its job with quite nice flexibility. It doesn't require any Xcode setup, works well with SPM too. The idea could be also extended to customize not only the Locale
, but also e.g. preferredlanguages
. The key point is that we can do it by exchanging the methods on NSLocale
instead of Locale
which still is used as a wrapper over the objective-c predecessor.
We can use method_setImplementation
and @convention(block)
(swift documentation) in this case, which makes it a little bit more ergonomic than defining the @objc
method that would solve the purpose of the exchanged method.
extension XCTestCase {
func setLocale(identifier: String, preferredLanguages: [String]) {
let currentlLocale: @convention(block) (AnyObject)
-> AnyObject = { (_: AnyObject!) -> NSLocale in
return NSLocale(localeIdentifier: identifier)
}
method_setImplementation(
class_getClassMethod(NSLocale.self, #selector(getter: NSLocale.current))!,
imp_implementationWithBlock(currentlLocale)
)
let preferredLanguages: @convention(block) (AnyObject)
-> [String] = { (_: AnyObject!) -> [String] in
return preferredLanguages
}
method_setImplementation(
class_getClassMethod(NSLocale.self, #selector(getter: NSLocale.preferredLanguages))!,
imp_implementationWithBlock(preferredLanguages)
)
}
}
Having that we can easily change the Locale
for each test. The only downside is that you still use singleton Locale.current
, so running tests in parallel will not work reliably.
class Test: XCTestCase {
func test_locale() {
setLocale(identifier: "fr", preferredLanguages: ["fr", "de", "pl"])
XCTAssertEqual(Locale.current, .init(identifier: "fr"))
XCTAssertEqual(Locale.preferredLanguages, ["fr", "de", "pl"])
}
}